package com.jzo2o.market.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.*;
import com.jzo2o.market.constants.TabTypeConstants;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.mapper.ActivityMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.dto.request.ActivityQueryForPageReqDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponWriteOffService;
import com.jzo2o.mysql.utils.PageUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;
import static com.jzo2o.market.enums.ActivityStatusEnum.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author itcast
 * @since 2023-09-16
 */
@Service
public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> implements IActivityService {
    private static final int MILLION = 1000000;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private ICouponService couponService;

    @Resource
    private ICouponWriteOffService couponWriteOffService;

    /**
     * 从缓存中获取活动信息
     * @param id
     * @return
     */
    @Override
    public ActivityInfoResDTO getActivityInfoByIdFromCache(Long id) {
        //活动信息
        Object activityListJson = redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);
            //活动无效的情况
        if(ObjectUtils.isNull(activityListJson)){
            return null;
        }
        //将json转成list
        List<ActivityInfoResDTO> activityInfoResDTOS = JsonUtils.toList(activityListJson.toString(), ActivityInfoResDTO.class);
        if(CollUtils.isEmpty(activityInfoResDTOS)){
            return null;
        }
        //从activityInfoResDTOS找到该活动
        ActivityInfoResDTO activityInfoResDTO = activityInfoResDTOS.stream().filter(v -> v.getId().equals(id)).findFirst().orElse(null);

        return activityInfoResDTO;
    }

    
    /**
     * 扣减库存
     * @param id 活动id
     *  如果扣减库存失败抛出异常
     */
    @Override
    public void deductStock(Long id) {
        // update activity t set t.stock_num = t.stock_num-1 where t.id=? and t.stock_num>0
        boolean update = lambdaUpdate()
                .setSql("stock_num = stock_num-1")
                .eq(Activity::getId, id)
                .gt(Activity::getStockNum, 0)
                .update();

        if(!update){
            throw new CommonException("修改活动"+id+"库存失败");
        }

    }

    /**
     * 活动预热
     */
    @Override
    public void preHeat() {
        //拼接下面的sql语句
        /**
         select *
         from activity t
         where t.distribute_start_time <= date_add(now(), interval 30 day)
         and t.status in (1, 2)
         order by t.distribute_start_time asc
         */

        //查询符合条件的活动
        List<Activity> list = lambdaQuery()
                .le(Activity::getDistributeStartTime, LocalDateTime.now().plusDays(30))
                .in(Activity::getStatus, Arrays.asList(1, 2))
                .orderByAsc(Activity::getDistributeStartTime)
                .list();
        if (CollUtils.isEmpty(list)) {
            //防止缓存穿透
            list = new ArrayList<>();
        }
        //将List<Activity>转List<SeizeCouponInfoResDTO>
        List<SeizeCouponInfoResDTO> seizeCouponInfoResDTOS = BeanUtils.copyToList(list, SeizeCouponInfoResDTO.class);
        //将SeizeCouponInfoResDTO转成json
        String seizeCouponInfoResString = JsonUtils.toJsonStr(seizeCouponInfoResDTOS);
        //将查询到的活动存入redis
        redisTemplate.opsForValue().set(ACTIVITY_CACHE_LIST,seizeCouponInfoResString);

        //对未开始的活动的库存直接更新到redis

        list.stream().filter(v-> NO_DISTRIBUTE.getStatus()==getStatus(v.getDistributeStartTime(),v.getDistributeEndTime(),v.getStatus())).forEach(v->{
            String key = String.format("COUPON:RESOURCE:STOCK:{%s}",v.getId()%10);
            redisTemplate.opsForHash().put(key,v.getId(),v.getTotalNum());
        });

        //对已经开始的活动库存,如果redis未设置库存才进行设置
        list.stream().filter(v-> DISTRIBUTING.getStatus()==getStatus(v.getDistributeStartTime(),v.getDistributeEndTime(),v.getStatus())).forEach(v->{
            String key = String.format("COUPON:RESOURCE:STOCK:{%s}",v.getId()%10);
            //putIfAbsent如果未设置才进行设置
            redisTemplate.opsForHash().putIfAbsent(key,v.getId(),v.getTotalNum());
        });

    }

    Object o = new Object();  //下面143行,用到的锁
    /**
     * 抢券界面查询活动信息
     * @param tabType 1：疯抢中，2：即将开始
     * @return
     */
    @Override
    public List<SeizeCouponInfoResDTO> queryForListFromCache(Integer tabType) {
        //查询redis中的活动列表 ACTIVITY:LIST (json串)
        Object seizeCouponInfoStr  = redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);

        if(ObjectUtils.isNull(seizeCouponInfoStr)){
            return Collections.emptyList();
        }

        //确定要查询的状态(对应数据库的状态）前端传入进行中的状态为1，对应到数据库的活动状态为2，前端传入的是2对应到数据库的活动状态为1
            //这里是因为前端传过来的状态1是,活动进行中,而我们后端数据库表中活动进行中的状态是2,,所以要单独设置一下,而且字段名字也不一样
        int queryStatus= tabType==1? DISTRIBUTING.getStatus(): NO_DISTRIBUTE.getStatus();

        //将json转成List<SeizeCouponInfoResDTO>
        List<SeizeCouponInfoResDTO> seizeCouponInfoResDTOS = JsonUtils.toList(seizeCouponInfoStr.toString(), SeizeCouponInfoResDTO.class);

        //根据tabType过滤活动列表，在过滤过程中根据活动的开始时间、结束时间及当前时间判断出准确状态，
            // 将这个准确的状态写入SeizeCouponInfoResDTO对象,最终才能返回
            //这个从数据库查询的状态,可能不够及时,,因此这里我们通过当前时间,活动开始时间和活动结束时间,判断一下准确的状态
        List<SeizeCouponInfoResDTO> collect = seizeCouponInfoResDTOS.stream().filter
                        (v -> queryStatus == getStatus(v.getDistributeStartTime(), 
                                v.getDistributeEndTime(), v.getStatus()))
                .peek(v -> { //peek是中间的一个过渡方法,也就是上面取出的每一个元素都会执行这个peek方法
                    //库存(设置剩余数量)
                    //下面单独设置属性的原因,是因为数据库中对应的字段和前端要的的不一致,就是名字不一致,所以得把拿到的值设置一下;看传入的参数名,也能看出来这两个字段名不一样
                    v.setRemainNum(v.getStockNum());   //这里相当于那数据库中的StockNum字段放到RemainNum字段,返回给前端,不是更改数据库
                    v.setStatus(queryStatus);
                }).collect(Collectors.toList());
            //上面整合的集合,,就是我们要返回的内容,,直接返回就行了

        //多线程中,,每个线程都会访问下面的代码,
            //谁访问这个代码,就会获取这个锁;相当于多个线程同时争抢这个锁;没抢到锁的就会在后面等待,然后阻塞
            //最好直接听老师视频,讲锁是什么,这里为啥要考虑用这种锁,这里代码怎么就实现了锁的功能;怎么用,怎么定义的知识点,等等
            //锁不熟悉,,可以回顾讲义: 第八章 秒杀抢购讲义__2.3.1解决超卖问题
            //对应视频: day10_5.解决超卖问题,大约17min左右
        synchronized (o){
            //扣减库存
            //....    //这里表示具体的扣减库存的代码就不写了,,就是演示一下,可以用锁
        }

        return collect;
    }

    /**
     * 单独定义的判断状态的方法,,上面有调用到
     * 还是个私有方法,那就只当前类能用
     * @param distributeStartTime
     * @param distributeEndTime
     * @param status
     * @return
     */
    //根据活动的开始时间、结束时间、数据库中的状态 判断出的准确的状态
    private int getStatus(LocalDateTime distributeStartTime, LocalDateTime distributeEndTime, Integer status){
        //当前时间
        LocalDateTime now = LocalDateTime.now();
        
        //如果当前状态是待生效，如果开始时间小于当前时间 并且结束时间大于当前时间 活动已经开始了，并且还没结束,,返回的状态是进行中
        if(status == NO_DISTRIBUTE.getStatus()
                && distributeStartTime.isBefore(now)
                && distributeEndTime.isAfter(now)){  
            return DISTRIBUTING.getStatus();
        }else if(status == NO_DISTRIBUTE.getStatus()
                && distributeEndTime.isBefore(now)){
            //如果当前状态是待生效，如果结束时间小于当前时间，活动已经结束了，返回的状态是已结束
            return LOSE_EFFICACY.getStatus();
        }else if(status == DISTRIBUTING.getStatus()
                && distributeEndTime.isBefore(now)){
            //如果当前状态是进行中，如果结束时间小于当前时间，活动已经结束了，返回的状态是已结束
            return LOSE_EFFICACY.getStatus();
        }
        //如果都不符合,,那就传进来什么再返回什么状态
        return status;
    }

    
    @Override
    public PageResult<ActivityInfoResDTO> queryForPage(ActivityQueryForPageReqDTO activityQueryForPageReqDTO) {
        LocalDateTime now = DateUtils.now();
        // 1.查询准备
        LambdaQueryWrapper<Activity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 查询条件
        lambdaQueryWrapper.eq(ObjectUtils.isNotNull(activityQueryForPageReqDTO.getId()), Activity::getId, activityQueryForPageReqDTO.getId())
                .like(StringUtils.isNotEmpty(activityQueryForPageReqDTO.getName()), Activity::getName, activityQueryForPageReqDTO.getName())
                .eq(ObjectUtils.isNotNull(activityQueryForPageReqDTO.getType()), Activity::getType, activityQueryForPageReqDTO.getType())
                .eq(ObjectUtils.isNotNull(activityQueryForPageReqDTO.getStatus()), Activity::getStatus, activityQueryForPageReqDTO.getStatus());

        // 排序
        lambdaQueryWrapper.orderByDesc(Activity::getId);
        // 分页
        Page<Activity> activityPage = new Page<>(activityQueryForPageReqDTO.getPageNo().intValue(), activityQueryForPageReqDTO.getPageSize().intValue());
        activityPage = baseMapper.selectPage(activityPage, lambdaQueryWrapper);
        return PageUtils.toPage(activityPage, ActivityInfoResDTO.class);
    }

    @Override
    public ActivityInfoResDTO queryById(Long id) {
        // 1.获取活动
        Activity activity = baseMapper.selectById(id);
        // 判空
        if (activity == null) {
            return new ActivityInfoResDTO();
        }
        // 2.数据转换，并返回信息
        ActivityInfoResDTO activityInfoResDTO = BeanUtils.toBean(activity, ActivityInfoResDTO.class);
        // 设置状态
//        activityInfoResDTO.setStatus(getStatus(activity.getDistributeStartTime(), activity.getDistributeEndTime(), activity.getStatus()));
        // 3.领取数量
//        Integer receiveNum = couponService.countReceiveNumByActivityId(activity.getId());
        Integer receiveNum = activity.getTotalNum()-activity.getStockNum();
        activityInfoResDTO.setReceiveNum(receiveNum);
        // 4.核销量
        Integer writeOffNum = couponWriteOffService.countByActivityId(id);
        activityInfoResDTO.setWriteOffNum(NumberUtils.null2Zero(writeOffNum));

        //
        return activityInfoResDTO;
    }

    @Override
    public void save(ActivitySaveReqDTO activitySaveReqDTO) {
        // 1.逻辑校验
        activitySaveReqDTO.check();
        // 2.活动数据组装
        // 转换
        Activity activity = BeanUtils.toBean(activitySaveReqDTO, Activity.class);
        // 状态
        activity.setStatus(NO_DISTRIBUTE.getStatus());
        //库存
        activity.setStockNum(activitySaveReqDTO.getTotalNum());
        if(activitySaveReqDTO.getId() == null) {
            activity.setId(IdUtils.getSnowflakeNextId());
        }
        //排序字段
//        long sortBy = DateUtils.toEpochMilli(activity.getDistributeStartTime()) * MILLION + activity.getId() % MILLION;
        // 3.保存
        saveOrUpdate(activity);
    }


    @Override
    public void updateStatus() {
        LocalDateTime now = DateUtils.now();
        // 1.更新已经进行中的状态
        lambdaUpdate()
                .set(Activity::getStatus, ActivityStatusEnum.DISTRIBUTING.getStatus())//更新活动状态为进行中
                .eq(Activity::getStatus, NO_DISTRIBUTE)//检索待生效的活动
                .le(Activity::getDistributeStartTime, now)//活动开始时间小于等于当前时间
                .gt(Activity::getDistributeEndTime,now)//活动结束时间大于当前时间
                .update();
        // 2.更新已经结束的
        lambdaUpdate()
                .set(Activity::getStatus, LOSE_EFFICACY.getStatus())//更新活动状态为已失效
                .in(Activity::getStatus, Arrays.asList(DISTRIBUTING.getStatus(), NO_DISTRIBUTE.getStatus()))//检索待生效及进行中的活动
                .lt(Activity::getDistributeEndTime, now)//活动结束时间小于当前时间
                .update();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void revoke(Long id) {
        // 1.活动作废
        boolean update = lambdaUpdate()
                .set(Activity::getStatus, ActivityStatusEnum.VOIDED.getStatus())
                .eq(Activity::getId, id)
                .in(Activity::getStatus, Arrays.asList(NO_DISTRIBUTE.getStatus(), DISTRIBUTING.getStatus()))
                .update();
        if(!update) {
            return;
        }
        // 2.未使用优惠券作废
        couponService.revoke(id);

    }

}
